from google.appengine.ext import db
from google.appengine.api import datastore

class ListReferenceProperty(db.Property):
    """A property that represents a many-to-many reference to another model.

    for example, a reference property in model A that refers to model B forms
    a many-to-many relationship from A to B: every instance of A refers to
    many instances of B, and every B instance can refer to many A instances.
    """

    def __init__(self,
                 reference_class=None,
                 verbose_name=None,
                 collection_name=None,
                 default=list(),
                 **attrs):
        """Construct ReferenceProperty.

        Args:
          reference_class: Which model class this property references.
          verbose_name: User friendly name of property.
          collection_name: If provided, alternate name of collection on
            reference_class to store back references.  Use this to allow
            a Model to have multiple fields which refer to the same class.
          default: the default list to use.
        """
        super(ListReferenceProperty, self).__init__(verbose_name,
                                                    default=default, **attrs)

        self.collection_name = collection_name

        if reference_class is None:
            reference_class = db.Model
        if not ((isinstance(reference_class, type) and
                 issubclass(reference_class, db.Model)) or
                reference_class is db._SELF_REFERENCE):
            raise KindError('reference_class must be Model or _SELF_REFERENCE')
        self.reference_class = self.data_type = reference_class

    def __property_config__(self, model_class, property_name):
        """Loads all of the references that point to this model.

        We need to do this to create the ReverseReferenceProperty properties for
        this model and create the <reference>_set attributes on the referenced
        model, e.g.:

           class Author(db.Model):
             name = db.StringProperty()
           class Book(db.Model):
             title = db.StringProperty()
             authors = db.ListReferenceProperty(Author)
           author = Author.get(id)
           print [b for b in author.book_set]
           or
           book = Book.get(id)
           print [a for a in book.authors.all()]

        In this example, the book_set property was created based on the reference
        from Book to Author (which is inherently many to many).

        Args:
          model_class: Model class which will have its reference properties
            initialized.
          property_name: Name of property being configured.

        Raises:
          DuplicatePropertyError if referenced class already has the provided
            collection name as a property.
        """
        super(ListReferenceProperty, self).__property_config__(model_class,
                                                               property_name)

        if self.reference_class is db._SELF_REFERENCE:
            self.reference_class = self.data_type = model_class

        if self.collection_name is None:
            self.collection_name = '%s_set' % (model_class.__name__.lower())
        existing_prop = getattr(self.reference_class, self.collection_name,
                                None)
        if existing_prop is not None:
            if not (isinstance(existing_prop,
                               db._ReverseReferenceProperty) and
                    existing_prop._prop_name == property_name and
                    existing_prop._model.__name__ == model_class.__name__ and
                    existing_prop._model.__module__ == model_class.__module__):
                raise db.DuplicatePropertyError('Class %s already has property %s '
                                                % (self.reference_class.__name__,
                                                   self.collection_name))
        setattr(self.reference_class,
                self.collection_name,
                db._ReverseReferenceProperty(model_class, property_name))

    def get_value_for_datastore(self, model_instance):
        """A list of key values is stored.
        Prior to storage, we validate the items in the list.
        This check seems to be missing from ListProperty.
        Args:
          model_instance: Instance to fetch datastore value from.
        Returns:
          A list of the keys for all Models in the value list.
        """
        value = self.__get__(model_instance, model_instance.__class__)
        self.validate(value)
        if value is None:
            return None
        else:
            return [v.key() if hasattr(v, 'key') else v for v in value]

    def make_value_from_datastore(self, value):
        """Recreates the list of Models from the list of keys."
        Args:
          value: value retrieved from the datastore entity.
        Returns:
          None or a list of Models.
        """
        if value is None:
            return None
        else:
            gql = "WHERE __key__ IN :1"
            return self.reference_class.gql(gql, value)

    def validate(self, value):
        """Validate reference.

        Returns:
          A valid value.

        Raises:
          BadValueError for the following reasons:
            - Value is not saved.
            - Object not of correct model type for reference.
        """
        value = super(ListReferenceProperty, self).validate(value)
        if value is not None:
            if not isinstance(value, list):
                raise BadValueError('Property %s must be a list' % self.name)

            value = self.validate_list_contents(value)
        return value

    def validate_list_contents(self, value):
        """Validates that all items in the list are of the correct type.

        Returns:
          The validated list.

        Raises:
          BadValueError if the list has items are not instances of the
          item_type given to the constructor.
        """
        for item in value:
            if isinstance(item, datastore.Key):
                continue
            elif item is not None and not item.has_key():
                raise db.BadValueError(
                    '%s instance must have a complete key before it can be stored as a '
                    'reference' % self.reference_class.kind())

            if item is not None and not isinstance(item, self.reference_class):
                raise db.KindError('Property %s must be an instance of %s' %
                                   (self.name, self.reference_class.kind()))
        return value

    def empty(self, value):
        """Is list property empty.

        [] is not an empty value.

        Returns:
          True if value is None, else false.
        """
        return value is None

    data_type = list

    def default_value(self):
        """Default value for list.

        Because the property supplied to 'default' is a static value,
        that value must be shallow copied to prevent all fields with
        default values from sharing the same instance.

        Returns:
          Copy of the default value.
        """
        return list(super(ListReferenceProperty, self).default_value())
